﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using Ping.Core;
using Ping.Server.Cron;
using log4net;

namespace Ping.Server
{
	/// <summary>
	/// Represents one server instance that listens for incoming connections and then creates one
	/// Connection per connection that handles the communication.
	/// </summary>
	public sealed class Server
		: IServer
	{
		private static readonly ILog Log = LogManager.GetLogger(MethodBase.GetCurrentMethod().DeclaringType);

		private readonly DateTime _started;
		private readonly List<Connection> _connections;
		private readonly TcpListener _listener;
		private readonly JobScheduler _jobScheduler;
		private readonly Blacklist _blacklist;

		/// <summary>
		/// The number of established connections.
		/// </summary>
		public int NumConnections { get { return _connections.Count; } }
		public int NumBlacklistedIPs { get { return _blacklist.NumBlocked; } }
		public DateTime Started { get { return _started; } }

		public Server(IPEndPoint ep, Blacklist blacklist)
		{
			if (ep == null || blacklist == null)
				throw new ArgumentNullException();

			Log.InfoFormat("Listening on: {0}", ep);

			_started = DateTime.Now;
			_listener = new TcpListener(ep);
			_connections = new List<Connection>();
			_jobScheduler = new JobScheduler(this);
			_blacklist = blacklist;
		}

		/// <summary>
		/// Endlessly listens on incoming connections...
		/// </summary>
		/// <remarks>
		/// This method never returns.
		/// </remarks>
		public void Start()
		{
			_listener.Start();

			try
			{
				while (true)
				{
					var client = _listener.AcceptTcpClient();
					var ep = (IPEndPoint) client.Client.RemoteEndPoint;
					if (_blacklist.IsBlocked(ep.Address))
					{
						Log.InfoFormat("Blocking connection from {0}", ep.Address);
						client.Close();
						continue;
					}

					var connection = new Connection(client, _jobScheduler);
					try
					{
						Log.InfoFormat("Incoming connection from client({0})", connection.RemoteEndPoint);

						connection.Closed += ConnectionClosed;
						_connections.Add(connection);
						connection.Start();
					}
					catch (Exception e)
					{
						// In case the connection couldn't be started we have to:
						// -remove the connection from the list of active connections
						// -dispose of the connection to free all resources associated with it
						// -log the event

						_connections.Remove(connection);
						connection.Closed -= ConnectionClosed;

						Log.ErrorFormat("Unhandled exception caught while starting connection to {0}: {1}", connection.RemoteEndPoint, e);

						throw;
					}
				}
			}
			catch(Exception e)
			{
				Log.FatalFormat("Server shutting down because of an unhandled exception: {0}", e);
			}
		}

		/// <summary>
		/// Is called whenever a connection has been closed.
		/// </summary>
		/// <param name="connection"></param>
		private void ConnectionClosed(Connection connection)
		{
			if (connection == null)
				throw new ArgumentNullException();

			_connections.Remove(connection);
			connection.Closed -= ConnectionClosed;

			Log.InfoFormat("Closing connection to client({0})", connection.RemoteEndPoint);
		}
	}
}